查看原文
其他

Linux进程间通信(中)之信号、信号量实践

刘凯强 嵌入式云IOT技术圈 2021-01-31

上节我们分享了Linux进程间通信的其中两种方式:管道、消息队列,文章如下:

Linux进程间通信(上)之管道、消息队列实践

这节我们就来分享一下Linux的另外两种进程间通信的方式:信号、信号量。

1、信号

我们使用过windows的都知道,当一个程序被卡死的时候不管怎样都没反应,这样我们就可以打开任务管理器直接强制性的结束这个进程,这个方法的实现就是和Linux上通过生成信号和捕获信号来实现相似的,运行过程中进程捕获到这些信号做出相应的操作使最终被终止。

信号的主要来源是分为两部分,一部分是硬件来源,一部分是软件来源;进程在实际中可以用三种方式来响应一个信号:一是忽略信号,不对信号做任何操作,其中有两个信号是不能别忽略的分别是SIGKILL和SIGSTOP。二是捕捉信号,定义信号处理函数,当信号来到时做出响应的处理。三是执行缺省操作,Linux对每种信号都规定了默认操作。注意,进程对实时信号的缺省反应是立即终止。

发送信号的函数有很多,主要使用的有:kill()、raise()、abort()、alarm()

先来熟悉下kill函数,进程可以通过kill()函数向包括它本身在内的其它进程发送一个信号,如果程序没有发送这个信号的权限,对kill函数的调用将会失败,失败的原因通常是由于目标进程由另一个用户所拥有。

kill函数的原型为:

#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);

它的作用是把信号sig发送给进程号为pid的进程,成功时返回0。kill调用失败返回-1,调用失败通常有三大原因:

  • 1、给定的信号无效
  • 2、发送权限不够
  • 3、目标进程不存在

还有一个非常重要的函数,信号处理signal函数。程序可以用signal函数来处理指定的信号,主要通过恢复和忽略默认行为来操作。signal函数原型如下:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

我们来看一个例程了解一下signal函数。signal.c

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

//函数ouch对通过参数sig传递进来的信号作出响应。
void ouch(int sig)
{
 printf("signal %d\n", sig);
 //恢复终端中断信号SIGINT的默认行为
 (void) signal(SIGINT, SIG_DFL);
}
int main()
{
  //改变终端中断信号SIGINT的默认行为,使之执行ouch函数
  (void) signal(SIGINT, ouch);
 
  while(1)
  {
   printf("Hello World!\n");
   sleep(1); 
  }
 return 0;
}

运行结果:


可以看出当我按下ctrl+c的时候并不会退出,只有当再次按下ctrl+c的时候才会退出。造成的原因是因为SIGINT的默认行为被signal函数改变了,当进程接受到信号SIGINT时,它就去调用函数ouch去处理,注意ouch函数把信号SIGINT的处理方式改变成默认的方式,所以当你再按一次ctrl+c时,进程就像之前那样被终止了。

下面是几种常见的信号:

  • SIGHUP :从终端上发出的结束信号
  • SIGINT :来自键盘的中断信号 ( ctrl + c )
  • SIGKILL :该信号结束接收信号的进程
  • SIGTERM:kill 命令发出的信号
  • SIGCHLD:标识子进程停止或结束的信号
  • SIGSTOP:来自键盘 ( ctrl + z ) 或调试程序的停止执行信号。

信号发送主要函数有kill和raise。上面我们知道kill函数的用法也清楚kill函数是可以向自身发送信号和其它进程发送信号,raise与之不同的是只可以向本身发送信号。

通过raise函数向自身发送数据,使子进程暂停通过测试如下: raise.c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  pid_t pid;
  int ret;
  if((pid=fork())<0)
  {
   printf("Fork error\n");
   exit(1);
  }
  //子进程
  if(pid==0)
  {
   //在子进程中使用raise()函数发出SIGSTOP信号,使子进程暂停
   printf("I am child pid:%d.I am waiting for any signal\n",getpid());
   raise(SIGSTOP);
   printf("I am child pid:%d.I am killed by progress:%d\n",getpid(),getppid());
   exit(0);
  }
  //父进程
  else  
  {
   sleep(2);  
   //在父进程中收集子进程发出的信号,并调用kill()函数进行相应的操作
   if((waitpid(pid,NULL,WNOHANG))==0) 
   { 
  //若pid指向的子进程没有退出,则返回0,且父进程不阻塞,继续执行下边的语句
    if((ret=kill(pid,SIGKILL))==0)
    {
     printf("I am parent pid:%d.I am kill %d\n",getpid(),pid);
    }
   }
   //等待子进程退出,否则就一直阻塞
   waitpid(pid,NULL,0);
   exit(0);
  }
}

当调用raise的时候子进程就会暂停:

信号是对终端机的一种模拟,也是一种异步通信方式。


2、信号量

主要作为进程间,以及同一进程不同线程之间的同步手段。信号量是用来解决进程之间的同步与互斥问题的一种进程之间的通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作。信号量对应于某一种资源,取一个非负的整形值。信号量的值是指当前可用的资源数量。

由于信号量只有两种操作,一种是等待信号,另一种是发送信号。即P和V,它们的行为如下:

  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。

Linux特别提供了一组信号量接口来对信号操作,它们不只是局限的针对二进制信号量,下面我们来对每个函数介绍,需要注意的是这些函数都是用来成对组的信号量值进行操作的。

2.1、semget函数

它的作用是创建一个新信号量或取得一个已有信号量。

int semget(key_t key, int nsems, int semflg); 

第一个参数是key整数型,不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。

第二个参数是制定需要的信号数量,通常情况下为1。

第三个参数是一组标志位,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

semget函数成功返回一个相应信号标识符(非零),失败返回-1。

2.2、semop函数

它的作用是改变信号量的值。

int semop(int semid, struct sembuf *sops, unsigned nsops);

sops是一个指针,它指向这样一个数组:元素用来描述对semid代表的信号量集合中第几个信号进行怎么样的操作。nops规定该数组中操作的数量。

semop函数返回0表示成功,返回-1表示失败。

2.3、semctl函数

该函数用来直接控制信号量信息。

int semctl(int semid, int semnum, int cmd, …);

semget并不会初始化每个信号量的值,这个初始化必须通过SETVAL命令或SETALL命令调用semctl来完成。

例程:semctl.c

#include <stdio.h>
#include <linux/sem.h>
#define NUMS 10  

int get_sem_val(int sid,int semnum)//取得当前信号量
{  
  return(semctl(sid,semnum,GETVAL,0));  
}  

int main(void)
{  
  int I ;
  int sem_id;  
  int pid;  
  int ret;  
  struct sembuf sem_op;//信号集结构
  union semun sem_val;//信号量数值

  //建立信号量集,其中只有一个信号量
  sem_id = semget(IPC_PRIVATE,1,IPC_CREAT|0600);
  //IPC_PRIVATE私有,只有本用户使用,如果为正整数,则为公共的;1为信号集的数量;
  if (sem_id==-1)
  {  
    printf("create sem error!\n");  
    exit(1);      
  }  
  printf("create %d sem success!\n",sem_id);      
  //信号量初始化
  sem_val.val=1;  
  //设置信号量,0为第一个信号量,1为第二个信号量,...以此类推;SETVAL表示设置
   ret = semctl(sem_id,0,SETVAL,sem_val);  
  if (ret < 0){  
    printf("initlize sem error!\n");  
    exit(1);      
   }  
   //创建进程
  pid = fork();  
  if (pid < 0)
  {  
    printf("fork error!\n");  
    exit(1);             
  }  
  else if(pid == 0)
  {
      //一个子进程,使用者
      for ( i=0;i<NUMS;i++)
      {  
        sem_op.sem_num=0;  
        sem_op.sem_op=-1;  
        sem_op.sem_flg=0;  
        semop(sem_id,&sem_op,1);//操作信号量,每次-1                  
        printf("%d 使用者: %d\n",i,get_sem_val(sem_id,0));  
      }       
  }  
  else
  {
      //父进程,制造者
     for (i=0;i<NUMS;i++)
     {  
          sem_op.sem_num=0;  
          sem_op.sem_op=1;  
          sem_op.sem_flg=0;  
          semop(sem_id,&sem_op,1);//操作信号量,每次+1                   
          printf("%d 制造者: %d\n",i,get_sem_val(sem_id,0));  
     }       
 }  
 exit(0);  
}

运行结果:

信号量的出现就是保证资源在一个时刻只能有一个进程(线程),所以例子当中只有制造者在制造(+1操作)过程中,使用者这个进程是无法随sem_id进行操作的。也就是说信号量是协调进程对共享资源操作的,起到了类似互斥锁的作用,但却比锁拥有更强大的功能。

往期精彩

Linux进程间通信(上)之管道、消息队列实践

C语言三剑客之《C陷阱与缺陷》一书精华提炼

C语言三剑客之《C专家编程》一书精华提炼

【为宏正名】99%人都不知道的"##"里用法

【Linux系统编程】可重入和不可重入函数

觉得本次分享的文章对您有帮助,随手点[在看]并转发分享,也是对我的支持。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存